
/**		
 * 
 *	uk.co.leanmean.net.loading.QueueLoader
 *	
 *	@version 1.00 | Oct 12, 2009
 *	@author Justin Windle
 *  
 **/
 
package uk.co.lmfm.net.loading 
{
	import uk.co.lmfm.net.loading.events.QueueLoaderEvent;

	import flash.events.EventDispatcher;
	import flash.system.System;
	import flash.utils.Dictionary;

	/**
	 * QueueLoader is a simple class for managing a loading queue of data and media items.
	 * As yet there is no support for loading sound and using webservices, but 
	 * it will support images, videos, swfs, text and xml. The number of sequential 
	 * server connections can be managed and content can be cached.
	 * 
	 * @example
	 * A simple example of how to use QueueLoader
	 * 
	 *	<listing version="1.0">
	 *	var queue : QueueLoader = new QueueLoader();
	 *	
	 *	queue.addEventListener(QueueLoaderEvent.ITEM_COMPLETE, onItemComplete);
	 *	queue.addEventListener(QueueLoaderEvent.QUEUE_COMPLETE, onQueueComplete);
	 *	
	 *	var item1 : QueueLoaderItem = queue.addItem("http://farm2.static.flickr.com/1157/1137616638_df649df4a9.jpg");
	 *	var item2 : QueueLoaderItem = queue.addItem("http://farm4.static.flickr.com/3035/3011750033_3d90667025.jpg");
	 *	
	 *	// data.php will return xml, but because it is a php extension we need to tell the queue what file type it is
	 *	var item3 : QueueLoaderItem = queue.addItem("http://mysite.com/data.php?page=home", QueueLoader.FILETYPE_XML);
	 *	
	 *	// Bump an item to the top of the list using the priority property
	 *	item2.priority = queue.nextPriority();
	 *	
	 *	// Start loading the queue
	 *	queue.start();
	 *	
	 *	private function onItemComplete(event : QueueLoaderEvent) : void
	 *	{
	 *		switch(event.item.fileType)
	 *		{
	 *			case QueueLoader.FILETYPE_IMAGE :
	 *			
	 *				addChild(event.item.data); 
	 *				
	 *				break;
	 *			
	 *			case QueueLoader.FILETYPE_XML :				
	 *			
	 *				var data : XML = event.item.data;
	 *				
	 *				break;
	 *		}
	 *	}
	 *	
	 *	function onQueueComplete(event : QueueLoaderEvent) : void
	 *	{
	 *		// Destroy all loaders in the queue
	 *		queue.clear(true);
	 *	}
	 *	
	 *  </listing>
	 */
	public class QueueLoader extends EventDispatcher 
	{
		//	----------------------------------------------------------------
		//	PUBLIC STATIC CONSTANTS
		//	----------------------------------------------------------------
		
		/**
		 * File type will be detected automatically from the file path
		 */

		public static const FILETYPE_AUTO : String = 'auto';

		/**
		 * Constant for all image file types (jpg, png, bmp...)
		 */

		public static const FILETYPE_IMAGE : String = 'image';
		
		/**
		 * Constant for loading files as binary
		 */

		public static const FILETYPE_BINARY : String = 'binary';

		/**
		 * Constant for all video file types
		 */

		public static const FILETYPE_VIDEO : String = 'video';

		/**
		 * Constant for all text file types
		 */

		public static const FILETYPE_TEXT : String = 'text';

		/**
		 * Constant for the file type of a Flash movie (swf)
		 */

		public static const FILETYPE_SWF : String = 'swf';

		/**
		 * Constant for the XML file type
		 */

		public static const FILETYPE_XML : String = 'xml';

		//	----------------------------------------------------------------
		//	PRIVATE MEMBERS
		//	---------------------------------------------------------------- 		

		private var _cacheSize : int;
		private var _bytesTotal : int;
		private var _maxConnections : uint ;
		private var _highestPriority : Number = 1.0;
		private var _logger : Function = trace;
		private var _complete : Boolean = false;
		private var _stopped : Boolean = false;		private var _debug : Boolean = false;

		private var _index : Dictionary = new Dictionary();
		private var _active : Array = [];
		private var _queue : Array = [];		private var _cached : Array = [];
		private var _cache : Object = {};

		//	----------------------------------------------------------------
		//	CONSTRUCTOR
		//	----------------------------------------------------------------

		public function QueueLoader(__maxConnections : uint = 1, __cacheSize : int = -1)
		{
			_maxConnections = __maxConnections;
			_cacheSize = __cacheSize;
		}

		//	----------------------------------------------------------------
		//	PUBLIC METHODS
		//	----------------------------------------------------------------
		
		/**
		 * Adds an item to the queue and returns the QueueLoaderItem that is 
		 * created for this url.
		 * 
		 * @param url The path of the item to load
		 * @param fileType The type of file being loaded. Defaults to 'auto', which 
		 * will attempt to determine the file type from the url
		 * @see #FILETYPE_AUTO		 * @see #FILETYPE_IMAGE		 * @see #FILETYPE_VIDEO		 * @see #FILETYPE_TEXT		 * @see #FILETYPE_SWF		 * @see #FILETYPE_XML
		 * @param id An optional ID to use for retrieving the item later. If null, 
		 * the url of the item will be used as the ID. Any object, not only Strings, 
		 * can be used to identify this item.
		 * @param priority The priority of this item (leave unchanged to load in the 
		 * usual order). You can also change an items priority at any time using the 
		 * priority accessor for the item itself.
		 * @param cache Whether or not this item should be cached in memory for 
		 * fast retrieval if the item is loaded again (saves on URLRequests).
		 * @param timestamp Whether to append a timestamp to the url in order to 
		 * prevent browser caching (useful for files that will change on the server)
		 * @return The QueueLoaderItem created to load this file
		 */

		public function addItem(url : String, fileType : String = 'auto', id : * = null, priority : Number = 1.0, cache : Boolean = true, timestamp : Boolean = true) : QueueLoaderItem
		{
			id = id ? id : url;

			var item : QueueLoaderItem = new QueueLoaderItem(url, fileType, id, priority, cache, timestamp);
			item.addEventListener(QueueLoaderEvent.ITEM_PRIORISED, onItemPrioritised);
			
			_index[id] = item;
			_queue.push(item);
			
			if(_complete && !_stopped)
			{
				log("New items added. Resuming queue...");
				_complete = false;
				updateQueue();
			}
			
			return item;
		}

		/**
		 * Returns a number slightly higher than the current highest priority. Use this 
		 * to set the priority on QueueLoaderItems that you want to bump to the front of 
		 * the queue, for example: <code>item.priority = queue.nextPriority();</code> 
		 */

		public function nextPriority() : Number
		{
			return _highestPriority += 0.1;
		}

		/**
		 * Retrieves a QueueLoaderItem from the queue using the items ID
		 * 
		 * @param id The ID of the item to retrieve
		 * @return A QueueLoaderItem from the queue
		 */

		public function getItem(id : *) : QueueLoaderItem
		{
			return _index[id];
		}

		/**
		 * Starts the queue. All items will begin loading sequentially. You can 
		 * call the stop method any time to pause the queue, and subsequentially 
		 * calling start will resume the queue.
		 */

		public function start() : void
		{
			log("Queue started");
			
			_bytesTotal = 0;
			_stopped = false;
			
			dispatchEvent(new QueueLoaderEvent(QueueLoaderEvent.QUEUE_STARTED));
			sortQueue();
			updateQueue();
		}

		/**
		 * Stops the queue from loading. The queue will attempt to cancel any 
		 * currently loading items. The queue will remain intact and can be resumed 
		 * again at any time by calling start. Items can be added and removed as normal 
		 * when a queue has been stopped.
		 */

		public function stop() : void
		{
			log("Queue stopped");
			
			for (var i : int = 0;i < _active.length;i++)
			{
				stopItem(_active[i]);
				_queue.unshift(_active[i]);
			}
			
			_stopped = true;
			_active.length = 0;
			dispatchEvent(new QueueLoaderEvent(QueueLoaderEvent.QUEUE_STOPPED));
		}
		
		/**
		 * Removes all items from the cache. Note, this is the cache used internally 
		 * in order to minimise URLRequests and will not remove the loaded items from 
		 * the system memory. In order to do this, you will need to remove all external 
		 * references to the loaded content and then call the clear method of this 
		 * QueueLoader instance.
		 * @see #clear()
		 */

		public function clearCache() : void
		{
			_cache = {};
			_cached.length = 0;
			_bytesTotal = 0;
		}

		/**
		 * Clears all items from the queue and optionally destroys all items 
		 * also, removing references to them and unloading their content.
		 * 
		 * @param destroy Whether or not to destroy items as well as just clearing 
		 * them from the loading queue.
		 */

		public function clear(destroy : Boolean = true) : void
		{
			log("Clearing queue" + (destroy ? " and destroying all items" : ''));
			
			_bytesTotal = 0;
			_queue.length = 0;
			_active.length = 0;
			_cache = {};
			
			clearCache();
			
			for each (var item : QueueLoaderItem in _index)
			{
				stopItem(item);
				
				if(destroy) item.destroy();
				delete _index[item.id];
			}
			
			System.gc();
		}

		//	----------------------------------------------------------------
		//	PRIVATE METHODS
		//	----------------------------------------------------------------

		private function updateQueue() : void
		{
			_complete = false;
			_active.length = 0;
			
			if(_queue.length == 0)
			{
				_complete = true;
				dispatchEvent(new QueueLoaderEvent(QueueLoaderEvent.QUEUE_COMPLETE));
				
				return;
			}
			
			var item : QueueLoaderItem;
			
			for (var i : int = 0;i < _maxConnections;i++)
			{
				item = _queue[0];
				
				if(_cache[item.url])
				{
					item = _cache[item.url];
				
					var qEvent : QueueLoaderEvent = new QueueLoaderEvent(QueueLoaderEvent.ITEM_COMPLETE);
					qEvent.progress = 1.0;
					qEvent.item = item;
				
					dispatchEvent(qEvent);
					
					continue;
				}
				
				item.addEventListener(QueueLoaderEvent.ITEM_ERROR, onItemError);
				item.addEventListener(QueueLoaderEvent.ITEM_STARTED, onItemStarted);
				item.addEventListener(QueueLoaderEvent.ITEM_STOPPED, onItemStopped);
				item.addEventListener(QueueLoaderEvent.ITEM_COMPLETE, onItemComplete);
				item.addEventListener(QueueLoaderEvent.ITEM_PROGRESS, onItemProgress);
				
				item.start();
				
				_queue.shift();
				_active.push(item);
				
				if(_queue.length == 0) break;
			}
		}

		private function stopItem(item : QueueLoaderItem) : void
		{
			item.removeEventListener(QueueLoaderEvent.ITEM_ERROR, onItemError);
			item.removeEventListener(QueueLoaderEvent.ITEM_STARTED, onItemStarted);
			item.removeEventListener(QueueLoaderEvent.ITEM_STOPPED, onItemStopped);
			item.removeEventListener(QueueLoaderEvent.ITEM_COMPLETE, onItemComplete);
			item.removeEventListener(QueueLoaderEvent.ITEM_PROGRESS, onItemProgress);
			item.removeEventListener(QueueLoaderEvent.ITEM_PRIORISED, onItemPrioritised);
			item.stop();
		}

		private function sortQueue() : void
		{
			if(_queue.length < 1) return;
			
			_queue.sortOn('priority', Array.NUMERIC | Array.DESCENDING);
			
			try
			{
				_highestPriority = _queue[0].priority;
			}
			catch(error : TypeError)
			{
				_highestPriority = 1.0;
			}
		}

		private function checkCache() : void
		{
			if(_cacheSize == -1) return;
			
			if(_bytesTotal > _cacheSize)
			{
				log("Clearing data from cache. Current data size: " + _bytesTotal);
				
				var item : QueueLoaderItem;
				
				while(_bytesTotal > _cacheSize)
				{
					item = _cached.shift();
					_bytesTotal -= item.bytesTotal;
					_cache[item.url] = null;
					item.destroy();
				}
				
				log("Data removed from cache. Current data size: " + _bytesTotal);
			}
		}

		private function log(message : String) : void
		{
			if(debug)
			{
				try 
				{
					_logger.apply(this, [message]);
				}
				catch(error : Error)
				{
					;
				}
			}
		}

		//	----------------------------------------------------------------
		//	EVENT HANDLERS
		//	----------------------------------------------------------------

		private function onItemError(event : QueueLoaderEvent) : void
		{
			log("Error loading: " + event.item);
			
			dispatchEvent(event);
			stopItem(event.item);
			event.item.destroy();
			updateQueue();
		}

		private function onItemStarted(event : QueueLoaderEvent) : void
		{
			log("Item started: " + event.item);
			dispatchEvent(event);
		}

		private function onItemStopped(event : QueueLoaderEvent) : void
		{
			log("Item stopped: " + event.item);
			dispatchEvent(event);
		}

		private function onItemProgress(event : QueueLoaderEvent) : void
		{
			dispatchEvent(event);
		}

		private function onItemComplete(event : QueueLoaderEvent) : void
		{
			log("Item complete: " + event.item);
			
			var item : QueueLoaderItem = event.item;
			
			if(item.cache)
			{
				_bytesTotal += item.bytesTotal;
				_cache[item.url] = event.item;
				_cached.push(item);
			}
			
			item.removeEventListener(QueueLoaderEvent.ITEM_ERROR, onItemError);
			item.removeEventListener(QueueLoaderEvent.ITEM_STARTED, onItemStarted);
			item.removeEventListener(QueueLoaderEvent.ITEM_STOPPED, onItemStopped);
			item.removeEventListener(QueueLoaderEvent.ITEM_COMPLETE, onItemComplete);
			item.removeEventListener(QueueLoaderEvent.ITEM_PROGRESS, onItemProgress);
			item.removeEventListener(QueueLoaderEvent.ITEM_PRIORISED, onItemPrioritised);
			
			dispatchEvent(event);
			updateQueue();
			checkCache();
		}

		private function onItemPrioritised(event : QueueLoaderEvent) : void
		{
			log("Item prioritised: " + event.item + " priority: " + event.item.priority);
			dispatchEvent(event);
			sortQueue();
		}

		//	----------------------------------------------------------------
		//	PUBLIC ACCESSORS
		//	----------------------------------------------------------------
		
		/**
		 * The maximum amount of simultanious connections that can be opened 
		 * by this QueueLoader.
		 */

		public function get maxConnections() : uint
		{
			return _maxConnections;
		}

		public function set maxConnections(value : uint) : void
		{
			_maxConnections = value;
		}

		/**
		 * Whether the queue has been stopped
		 */

		public function get stopped() : Boolean
		{
			return _stopped;
		}

		/**
		 * Whether the queue is complete
		 */

		public function get complete() : Boolean
		{
			return _complete;
		}

		/**
		 * The maximum amount of bytes that should be cached by this QueueLoader. 
		 * Setting this to -1 will disable automatic caching.
		 */

		public function get cacheSize() : int
		{
			return _cacheSize;
		}

		public function set cacheSize(value : int) : void
		{
			_cacheSize = value;
			checkCache();
		}

		/**
		 * The total number of bytes loaded and cached by this QueueLoader
		 */

		public function get bytesTotal() : int
		{
			return _bytesTotal;	
		}

		/**
		 * Switching on debugging will send important information to the specified 
		 * logging framework (which defaults to the standard trace).
		 * 
		 * @see #logger
		 */

		public function get debug() : Boolean
		{
			return _debug;
		}

		public function set debug(value : Boolean) : void
		{
			_debug = value;
		}

		/**
		 * The logging method to call when important events occur, if debug is set 
		 * to true. This defaults to the native trace method, but any method, such 
		 * as one belonging to a prefered logging framework can be used. A String 
		 * (the message) will be sent to the specified logger function.
		 */

		public function get logger() : Function
		{
			return _logger;
		}

		public function set logger(value : Function) : void
		{
			_logger = value;
		}
	}
}
